Skip to content

fix: serialize LAMMPS HDF5 trajectories - #381

Open
njzjz-bot wants to merge 1 commit into
deepmodeling:masterfrom
njzjz-bot:fix/issue-355-lmp-hdf5-traj
Open

fix: serialize LAMMPS HDF5 trajectories#381
njzjz-bot wants to merge 1 commit into
deepmodeling:masterfrom
njzjz-bot:fix/issue-355-lmp-hdf5-traj

Conversation

@njzjz-bot

Copy link
Copy Markdown

Summary

  • route trajectory output through a backend hook
  • keep filesystem paths for standard LAMMPS runs
  • return trajectory text for HDF5 serialization
  • test both HDF5 output values and the unchanged standard path behavior

Tests

  • PYTHONPATH=tests python -m unittest -v tests.op.test_run_lmp.TestRunLmp.test_hdf5_outputs_dataset_values tests.op.test_run_lmp.TestRunLmp.test_success
  • isort --check-only dpgen2/op/run_lmp.py tests/op/test_run_lmp.py
  • git diff --check

Closes #355

Coding agent: Codex
Codex version: codex-cli 0.149.0
Model: gpt-5.6-sol
Reasoning effort: xhigh

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 23, 2026
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 59 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e539106-5efa-4af5-a734-2dc5e21546eb

📥 Commits

Reviewing files that changed from the base of the PR and between 6b01f29 and 22146e4.

📒 Files selected for processing (2)
  • dpgen2/op/run_lmp.py
  • tests/op/test_run_lmp.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Return trajectory text from RunLmpHDF5 so its output matches the declared HDF5 dataset contract.

Closes deepmodeling#355

Coding-Agent: Codex
Codex-Version: codex-cli 0.149.1
Model: gpt-5.6-sol
Reasoning-Effort: xhigh
@njzjz-bot
njzjz-bot force-pushed the fix/issue-355-lmp-hdf5-traj branch from bac4a98 to 22146e4 Compare August 26, 2026 11:00
@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Aug 26, 2026
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.52%. Comparing base (6b01f29) to head (22146e4).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #381      +/-   ##
==========================================
+ Coverage   84.43%   84.52%   +0.09%     
==========================================
  Files         104      104              
  Lines        6110     6114       +4     
==========================================
+ Hits         5159     5168       +9     
+ Misses        951      946       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

wanghan-iapcm

This comment was marked as outdated.

@wanghan-iapcm
wanghan-iapcm dismissed their stale review August 27, 2026 04:12

Retracted. This review was produced without running the mandated /code-review fan-out (the loop skill's section 2); the substitute process used instead has since been shown to miss findings and, in one case, to state a verified-sounding falsehood. Re-reviewing properly.

@wanghan-iapcm wanghan-iapcm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diagnosis in #355 is right and the hook is the right place to fix it, but the fix does not change what lands in the HDF5 file. I verified this by running dflow's real serialization path rather than reading it.

The fix is a no-op in every reachable configuration

dflow/python/utils.py handle_output_artifact does, for an Artifact(HDF5Datasets) output:

if isinstance(slices, list):
    items = [(str(s), v) for s, v in zip(slices, value)]
else:
    items = flatten(value).items()

and dflow.utils.flatten only recurses into list / dict. A scalar of any other type never reaches the create_dataset loop. Round-tripping the real function:

scalar Path    slices=0 (int)  -> keys = []        empty h5   <- master, i.e. the bug in #355
scalar str     slices=0 (int)  -> keys = []        empty h5   <- this PR
scalar ndarray slices=0 (int)  -> keys = []        empty h5   <- get_model_devi
[str]          slices=0 (int)  -> keys = ['0']     b'trajectory data'
[Path]         slices=0 (int)  -> keys = ['0']     attrs: type=file, path=..., dtype=utf-8
scalar any     slices=[0]      -> AssertionError
[str] / [Path] slices=[0]      -> keys = ['0']

group_size and pool_size both default to None (dpgen2/utils/step_config.py), so Slices("int('{{item}}')", ...) in dpgen2/superop/prep_run_lmp.py renders slices as a plain int and the flatten branch is taken. And in the grouped configuration the PR is equally a no-op, because a list of Path already serialized correctly before it — dflow's Path branch does the read_text itself. So there is no configuration in which this change alters the resulting .h5.

Downstream, TrajRenderLammps.get_confs computes ntraj = len(trajs) == 0 and loops zero times, so use_hdf5: true selects zero configurations every iteration, silently.

The shape that works is already in the tree

RunRelaxHDF5 is wired with the identical Slices("int('{{item}}')") in dpgen2/superop/prep_run_diffcsp.py and works, because RunRelax.execute builds trajs = [] / model_devis = [] and appends. That list-vs-scalar difference is the whole thing.

For the record on how long this has been broken

08d8d6e (#267, 2024-10-21) declared both traj and model_devi as Artifact(HDF5Datasets), added the get_model_devi hook, and added the use_hdf5 switch — all in one commit — while leaving "traj": work_dir / lmp_traj_name on the line above unconverted. Not drift; an omission from day one. grep -rn "HDF5\|hdf5" tests/ returns nothing, and use_hdf5 appears in no example, test or doc, so neither HDF5 subclass has ever been exercised.

Details inline. The three code findings share one root cause and one change fixes them together.

Comment thread dpgen2/op/run_lmp.py

def get_traj(self, traj_file):
"""Return trajectory text for serialization into an HDF5 dataset."""
return traj_file.read_text()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the line to change. read_text() returns a str, which is exactly as much a scalar as the Path it replaced, so flatten drops it and the dataset loop never runs. Confirmed against the installed dflow: flatten('trajectory data') is {}, and handle_output_artifact with Artifact(HDF5Datasets) and an int slices produces an .h5 with keys = [].

Suggest returning a list containing the Path, not the text:

def get_traj(self, traj_file):
    return [traj_file]

The list is what flatten needs. Keeping it a Path also routes through dflow's own Path branch, which read_text() bypasses:

if v.is_file():
    try:
        data = v.read_text(encoding="utf-8"); dtype = "utf-8"
    except Exception:
        data = np.void(v.read_bytes()); dtype = "binary"
    d = f.create_dataset(s, data=data)
    d.attrs["type"] = "file"; d.attrs["path"] = str(v); d.attrs["dtype"] = dtype

So you get the is_file() guard, a binary fallback for a non-UTF-8 dump, and the type/path attrs for free. As written, a missing dump raises a bare FileNotFoundError out of execute() and a non-UTF-8 dump raises UnicodeDecodeError — I reproduced both. Those are unlikely in practice since dpgen2 generates the dump directive itself, but there is no reason to give up the guards. [Path] still arrives at the consumer as decoded text, because HDF5Dataset.get_data() decodes on dtype == "utf-8" in both branches.

The docstring on this method also needs updating: "Return trajectory text for serialization into an HDF5 dataset" asserts that returning the text is what causes serialization, and that is what is not true. Note too that RunLmpHDF5 does not override execute, so it inherits RunLmp.execute's Returns section, which still documents traj and model_devi as Artifact(Path).

Comment thread dpgen2/op/run_lmp.py
@@ -412,3 +416,7 @@ def get_output_sign(cls):

def get_model_devi(self, model_devi_file):
return np.loadtxt(model_devi_file)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the identical defect and needs to move in the same change, not a follow-up. np.loadtxt returns an ndarray, which is not a list or dict, so flatten drops it too — keys = [].

The reason it cannot wait: if only traj becomes a list, the two artifacts arrive downstream with different lengths, and dpgen2/exploration/selector/conf_selector_frame.py:88-89 is

ntraj = len(trajs)
assert ntraj == len(model_devis)

I simulated all three states through the real handle_output_artifact + handle_input_artifact round trip with two tasks:

both scalar (current head)      trajs=0  model_devis=0   assert passes vacuously, silent data loss
traj list, model_devi scalar    trajs=2  model_devis=0   AssertionError mid-workflow
both lists                      trajs=2  model_devis=2   correct

So fixing traj alone is worse than fixing neither. return [np.loadtxt(model_devi_file)] alongside the traj change.

Comment thread tests/op/test_run_lmp.py
)
)

self.assertEqual(out["traj"], "trajectory data")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is not vacuous — I checked, reverting get_traj to the bare Path makes it fail with AssertionError: PosixPath('task_000/traj.dump') != 'trajectory data'. But it asserts one layer above the defect, so it is green while the artifact it exists to protect is empty.

out["traj"] is the OP's in-memory return value. The failure lives in dflow's serialization of that value, which this test never invokes. I fed the exact value the test asserts on into the real handle_output_artifact and got an .h5 with zero datasets.

Issue #355 asked for "a test for RunLmpHDF5.execute output types". The letter is satisfied; the contract is not. A round-trip assertion is what would have caught this and what would stop it regressing:

from dflow.python.utils import handle_output_artifact
handle_output_artifact("traj", out["traj"], Artifact(HDF5Datasets), slices=0, data_root=tmp)
# then open the produced .h5 and assert its key set is non-empty

Worth doing for model_devi in the same test. Minor, while you are here: this lives in TestRunLmp but exercises RunLmpHDF5; a separate TestRunLmpHDF5 class would read better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Code scan] Make RunLmpHDF5 return the trajectory type it declares

2 participants